package com.google.code.geocoder;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.code.geocoder.model.DirectionsRequest;
import com.google.code.geocoder.model.DirectionsResponse;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.URL;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class DirectionsGeocoder {
    private static final Log logger = LogFactory.getLog(Geocoder.class);

    private static final String DIRECTIONS_REQUEST_SERVER_HTTP = "http://maps.googleapis.com";
    private static final String DIRECTIONS_REQUEST_SERVER_HTTPS = "https://maps.googleapis.com";
    private static final String DIRECTIONS_REQUEST_QUERY_BASIC = "/maps/api/directions/json?sensor=false";
    private static final String ENCODING = "UTF-8";

    private final String clientId;
    private final Mac mac;

    public DirectionsGeocoder() {
        clientId = null;
        mac = null;
    }

    public DirectionsGeocoder(final String clientId, final String clientKey) throws InvalidKeyException {
        if (StringUtils.isBlank(clientId)) {
            throw new IllegalArgumentException("ClientId is not defined");
        }
        if (StringUtils.isBlank(clientKey)) {
            throw new IllegalArgumentException("ClientKey is not defined");
        }

        this.clientId = clientId;
        this.mac = getMAC(clientKey);
    }

    public DirectionsResponse directions(final DirectionsRequest directionsRequest) {
        try {
            final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();

            final String urlString = getURL(directionsRequest);

            return request(gson, urlString);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return null;
        }
    }

    protected DirectionsResponse request(Gson gson, String urlString) throws IOException {
        URL url = new URL(urlString);
        Reader reader = new BufferedReader(new InputStreamReader(url.openStream(), ENCODING));
        try {
            return gson.fromJson(reader, DirectionsResponse.class);
        } finally {
            reader.close();
        }
    }

    protected String getURL(final DirectionsRequest directionsRequest) throws UnsupportedEncodingException {
        if (logger.isTraceEnabled()) {
            logger.trace(directionsRequest);
        }
        final StringBuilder url = getURLQuery(directionsRequest);

        if (mac == null) {
            // add server name to URL
            url.insert(0, DIRECTIONS_REQUEST_SERVER_HTTP);
        } else {
            addClientIdAndSignURL(url);

            // add server name to URL
            url.insert(0, DIRECTIONS_REQUEST_SERVER_HTTPS);
        }

        if (logger.isTraceEnabled()) {
            logger.trace("FULL Request URL: " + url);
        }
        return url.toString();
    }

    protected StringBuilder getURLQuery(DirectionsRequest directionsRequest) throws UnsupportedEncodingException {
        final String origin = directionsRequest.getOrigin();
        final String destination = directionsRequest.getDestination();

        final StringBuilder url = new StringBuilder(DIRECTIONS_REQUEST_QUERY_BASIC);
        if (StringUtils.isNotBlank(origin)) {
            url.append("&origin=").append(URLEncoder.encode(origin, ENCODING));
        } else {
            throw new IllegalArgumentException("Origin must be defined");
        }
        if (destination != null) {
            url.append("&destination=").append(URLEncoder.encode(destination, ENCODING));
        } else {
            throw new IllegalArgumentException("Destination must be defined");
        }
        if (logger.isTraceEnabled()) {
            logger.trace("URL query: " + url);
        }
        return url;
    }

    protected void addClientIdAndSignURL(StringBuilder url) throws UnsupportedEncodingException {
        url.append("&client=").append(URLEncoder.encode(clientId, ENCODING));

        if (logger.isTraceEnabled()) {
            logger.trace("URL query to Sign: " + url);
        }

        byte[] sigBytes;
        synchronized (mac) {
            mac.update(url.toString().getBytes());
            sigBytes = mac.doFinal();
        }

        String signature = new String(Base64.encodeBase64(sigBytes));
        signature = signature.replace('+', '-');
        signature = signature.replace('/', '_');

        if (logger.isTraceEnabled()) {
            logger.trace("Signature: [" + url + "] for URL query " + url);
        }
        url.append("&signature=").append(signature);
    }

    protected static Mac getMAC(String clientKey) throws InvalidKeyException {
        try {
            byte[] key = clientKey.replace('-', '+').replace('_', '/').getBytes();

            byte[] decodedKey = Base64.decodeBase64(key);

            // Get an HMAC-SHA1 signing key from the raw key bytes
            final SecretKeySpec sha1Key = new SecretKeySpec(decodedKey, "HmacSHA1");

            // Get an HMAC-SHA1 Mac instance and initialize it with the HMAC-SHA1 key
            final Mac result = Mac.getInstance("HmacSHA1");
            result.init(sha1Key);

            return result;
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        }
    }
}
